# -*- coding: utf-8 -*-

#    OriginDB
#    Copyright (C) 2010  Jason Power <power.jg@gmail.com>
#    Copyright (C) 2010  Travis Gockel <travis@gockelhut.com>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with this program; if not, write to the Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import logging
from google.appengine.ext import db
from search import Searchable
import yaml

class NotUniqueError(Exception):
	def __init__(self, key):
		self.key = key
	def __str__(self):
		return key+ " not unique"

class NotAuthorizedError(Exception):
	def __init__(self, user, groups = []):
		self.user = user
		self.acceptable = groups
	
	def __str__(self):
		return "Not authorized: %s is not in an authorized group" % self.user

class MyModel(db.Model):
	_unique = set()
	_searchable = set()
	
	"""\brief Does this model require a certain group to be inserted?
	   If this is \c None, then inserting will not check that the user
	   is authenticated.  If this is a \c Set or \c str, then only the
	   user(s) will be allowed to insert.
	"""
	INSERT_REQUIRE_GROUP = None

	def dump(self, otherData={}):
		data = self.properties()
		for key in data.keys():
			try:
				data[key] = self.__getattribute__(key)
			except KeyError:
				pass
		data['key'] = str(self.key())
		data['type'] = self.__class__.__name__

		data.update(otherData)
		logging.info(data)
		output = yaml.dump(data)
		return output

	@classmethod
	def _insert(cls, yml, user=None):
		if type(yml) == str:
			data = yaml.load(yml)
		else:
			data = yml
		
		logging.info("Recieved data: " +str(data))
		for i in cls._unique:
			if i in data.keys() and not cls.isUnique(i, data[i]):
				raise NotUniqueError(i)
		
		if (cls.INSERT_REQUIRE_GROUP):
			if not user:
				raise NotAuthorizedError('Nobody')
			
			if type(cls.INSERT_REQUIRE_GROUP) == str:
				if user.privilege != cls.INSERT_REQUIRE_GROUP:
					raise NotAuthorizedError(user, cls.INSERT_REQUIRE_GROUP)
			else:
				if not user.privilege in cls.INSERT_REQUIRE_GROUP:
					raise NotAuthorizedError(user, cls.INSERT_REQUIRE_GROUP)
		
		obj = cls()
		for key in data:
			if key in obj.properties().keys():
				logging.info("Trying to set "+key+" to "+str(data[key]))
				obj.__setattr__(key, data[key])

		if user:
			obj.user = user

		obj.put()
		return obj

	@classmethod
	def insert(cls, yml, user):
		return cls._insert(yml, user)

	def update(self, yml, user):
		if type(yml) == str:
			data = yaml.load(yml)
		else:
			data = yml
		logging.info(data.keys())
		for i in self._unique:
			if i in data.keys() and not self.isUnique(i, data[i], self.key()):
				raise NotUniqueError(i)

		for key in data:
			if key in self.properties().keys():
				self.__setattr__(key, data[key])
				
		self.user = user
		
		self.put()

	@classmethod
	def _search(cls, yml):
		if type(yml) == str:
			data = yaml.load(yml)
		else:
			data = yml
		
		logging.info("Searching %i properties" % len(data))
		logging.info(data)
		if len(data) == 0:
			return [];
		
		keys = []
		query = "SELECT __key__ FROM " + cls.__name__ + " WHERE %s = :1"
		for prop in data.keys():
				if prop in cls._searchable:
						if type(data[prop])==str:
								data[prop] = data[prop]
						if type(data[prop])==bool:
								if data[prop]:
										data[prop] = "TRUE"
								else:
										data[prop] = "FALSE"
						logging.info(query % (prop))
						logging.info(str(data))
						q = db.GqlQuery(query % (prop), data[prop])
						keys.extend(q.fetch(1000))

		keys = [str(i) for i in keys]
		logging.info(keys)
		return keys

	@classmethod
	def search(cls, yml):
		return cls._search(yml)
	
	@classmethod
	def _find(cls, yml):
		keys = cls.search(yml)
		objs = []
		for k in keys:
			obj = cls.get(db.Key(k))
			objs.append(obj)
		return objs
	
	@classmethod
	def find(cls, yml):
		return cls._find(yml)

	@classmethod
	def isUnique(cls, property, value, key=None):
		if not value:
			return True
		query = cls.gql("WHERE "+property+" = :1", value)
		if not key:
			if query.count() > 0:
				return False
			else:
				return True
		else:
			a = query.get()
			if not a:
				return True
			elif a.key() == key:
				return True
			else:
				return False

class MySearchableModel(MyModel, Searchable):
	INDEX_IMMEDIATELY = True
	
	NON_INDEXED_SEARCHABLE = set([])
	
	@classmethod
	def search(cls, yml):
		if type(yml) == str:
			data = yaml.load(yml)
		else:
			data = yml
		
		if len(data) == 0:
			return [];
		
		#This allows us to filter on non-fulltext indexed properties
		#	If the user is searching on a non-indexed searchable, fall back
		#	to the regular search
		for prop in cls.NON_INDEXED_SEARCHABLE:
			if prop in data:
				return cls._search(data)
		
		ss = "";
		for prop in cls._searchable:
			if prop in data.keys():
				if type(data[prop]) == str:
					ss = ss + ' ' + str(data[prop]);
				elif type(data[prop]) == list or type(data[prop]) == set:
					for a in data[prop]:
						ss = ss + ' ' + a
				else:
					logging.warning("type(data['%s']) == %s" % (prop, type(data[prop])))
		ss = ss[1:];
		logging.info("Full text search for %s on '%s'" % (cls, ss))

		results = []
		for (i, j) in cls.full_text_search(ss):
			if (i.kind() == cls.kind()):
				results.append(i);
		results = [str(key) for key in results];
		logging.info(str(results));
		return results;
	
	@classmethod
	def insert(cls, yml, user):
		result = cls._insert(yml, user)
		
		if (cls.INDEX_IMMEDIATELY):
			result.index();
		else:
			result.enqueue_indexing(url='/tasks/searchindexing')
		
		return result;
